package org.jeecg.modules.demo.jxcx.Utils;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Base64;

public class WXPayAPI {

    private static Logger logger = LoggerFactory.getLogger(WXPayAPI.class);

    /**
     * 小程序下单
     *
     * @param appid        【公众号ID】
     * @param mchId        【直连商户号】
     * @param description  【商品描述】
     * @param outTradeNo   【商户订单号】商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一。
     * @param notifyUrl    【通知地址】 异步接收微信支付结果通知的回调地址，通知URL必须为外网可访问的URL，不能携带参数。 公网域名必须为HTTPS，如果是走专线接入，使用专线NAT IP或者私有回调域名可使用HTTP
     * @param total        【总金额】 订单总金额，单位为分。
     * @param openid       【用户标识】 用户在普通商户AppID下的唯一标识。
     * @param commonParams
     * @return prepay_id 【预支付交易会话标识】 预支付交易会话标识。用于后续接口调用中使用，该值有效期为2小时。
     * @throws Exception
     */
    public static String createOrder(String appid, String mchId, String description, String outTradeNo,
                                     String notifyUrl, BigDecimal total, String openid, String attach, JSONObject commonParams) throws Exception {

        String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("appid", appid);
        jsonObject.put("mchid", mchId);
        jsonObject.put("description", description);
        jsonObject.put("out_trade_no", outTradeNo);
        jsonObject.put("notify_url", notifyUrl);
        jsonObject.put("attach", attach);

        JSONObject amount = new JSONObject();
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);

        JSONObject payer = new JSONObject();
        payer.put("openid", openid);

        jsonObject.put("payer", payer);

        String result = doPost(url, jsonObject, commonParams);
        String prepayId = JSONObject.parseObject(result).getString("prepay_id");
        if (StringUtils.isEmpty(prepayId)) {
            throw new Exception("小程序下单异常，prepay_id为空");
        }
        return prepayId;
    }

    /**
     * 获取小程序调起支付所需的参数
     *
     * @param prepayId   【预支付交易会话标识】 预支付交易会话标识。用于后续接口调用中使用，该值有效期为2小时
     * @param appid      【公众号ID】
     * @param privateKey 【私钥字符串】
     * @return
     * @throws Exception
     */
    public static JSONObject getPayParams(String prepayId, String appid, String privateKey) throws Exception {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = RandomStringUtils.randomNumeric(32);
        ArrayList<Object> list = new ArrayList<>();
        list.add(appid);
        list.add(timeStamp);
        list.add(nonceStr);
        list.add("prepay_id=" + prepayId);
        //二次签名，调起支付需要重新签名
        StringBuilder stringBuilder = new StringBuilder();
        for (Object o : list) {
            stringBuilder.append(o).append("\n");
        }
        String signMessage = stringBuilder.toString();
        byte[] message = signMessage.getBytes();

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8"))));
        signature.update(message);
        String packageSign = Base64.getEncoder().encodeToString(signature.sign());

        //构造数据给微信小程序，用于调起微信支付
        JSONObject params = new JSONObject();
        params.put("timeStamp", timeStamp);
        params.put("nonceStr", nonceStr);
        params.put("package", "prepay_id=" + prepayId);
        params.put("signType", "RSA");
        params.put("paySign", packageSign);

        return params;
    }

    /**
     * 微信支付订单号查询订单
     *
     * @param transactionId 【微信支付订单号】 微信支付系统生成的订单号
     * @param mchId         【直连商户号】 直连商户的商户号，由微信支付生成并下发。
     * @param commonParams
     * @return
     * @throws Exception
     */
    public static JSONObject queryOrderByTransactionId(String transactionId, String mchId, JSONObject commonParams) throws Exception {
        //请求URL
        URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/id/" + transactionId);
        uriBuilder.setParameter("mchid", mchId);

        String result = doGet(uriBuilder, commonParams);
        return JSONObject.parseObject(result);
    }

    /**
     * 商户订单号查询订单
     *
     * @param outTradeNo   【商户订单号】 商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一。
     * @param mchId        【直连商户号】 直连商户的商户号，由微信支付生成并下发。
     * @param commonParams
     * @return
     * @throws Exception
     */
    public static JSONObject queryOrderByOutTradeNo(String outTradeNo, String mchId, JSONObject commonParams) throws Exception {
        //请求URL
        URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + outTradeNo);
        uriBuilder.setParameter("mchid", mchId);

        String result = doGet(uriBuilder, commonParams);
        return JSONObject.parseObject(result);
    }

    /**
     * 关闭订单
     *
     * @param outTradeNo   【商户订单号】 商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一。
     * @param mchId        【直连商户号】
     * @param commonParams
     * @return
     * @throws Exception
     */
    public static Boolean closeOrder(String outTradeNo, String mchId, JSONObject commonParams) throws Exception {

        String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + outTradeNo + "/close";

        JSONObject tradeParams = new JSONObject();
        tradeParams.put("mchid", mchId);

        String result = doPost(url, tradeParams, commonParams);
        if (StringUtils.isEmpty(result)) {
            return false;
        }
        return true;
    }

    /**
     * 退款申请（通过商户订单号）
     *
     * @param outTradeNo   【商户订单号】 原支付交易对应的商户订单号
     * @param outRefundNo  【商户退款单号】 商户系统内部的退款单号，商户系统内部唯一，只能是数字、大小写字母_-|*@ ，同一退款单号多次请求只退一笔。
     * @param reason       【退款原因】 若商户传入，会在下发给用户的退款消息中体现退款原因
     * @param refund       【退款金额】 退款金额，单位为分，只能为整数，不能超过原订单支付金额。
     * @param total        【原订单金额】 原支付交易的订单总金额，单位为分，只能为整数。
     * @param notifyUrl    【退款结果回调url】 异步接收微信支付退款结果通知的回调地址，通知url必须为外网可访问的url，不能携带参数。 如果参数中传了notify_url，则商户平台上配置的回调地址将不会生效，优先回调当前传的这个地址。
     * @param commonParams
     * @return
     * @throws Exception
     */
    public static JSONObject refundByOutTradeNo(String outTradeNo, String outRefundNo, String reason,
                                                BigDecimal refund, BigDecimal total, String notifyUrl, JSONObject commonParams) throws Exception {

        String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("out_trade_no", outTradeNo);
        jsonObject.put("out_refund_no", outRefundNo);
        jsonObject.put("reason", reason);

        JSONObject amount = new JSONObject();
        amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);
        jsonObject.put("notify_url", notifyUrl);

        String result = doPost(url, jsonObject, commonParams);

        return JSONObject.parseObject(result);
    }

    /**
     * 退款申请（通过微信支付订单号）
     *
     * @param transactionId 【微信支付订单号】 原支付交易对应的微信订单号
     * @param outRefundNo   【商户退款单号】 商户系统内部的退款单号，商户系统内部唯一，只能是数字、大小写字母_-|*@ ，同一退款单号多次请求只退一笔。
     * @param reason        【退款原因】 若商户传入，会在下发给用户的退款消息中体现退款原因
     * @param refund        【退款金额】 退款金额，单位为分，只能为整数，不能超过原订单支付金额。
     * @param total         【原订单金额】 原支付交易的订单总金额，单位为分，只能为整数。
     * @param notifyUrl     【退款结果回调url】 异步接收微信支付退款结果通知的回调地址，通知url必须为外网可访问的url，不能携带参数。 如果参数中传了notify_url，则商户平台上配置的回调地址将不会生效，优先回调当前传的这个地址。
     * @param commonParams
     * @return
     * @throws Exception
     */
    public static JSONObject refundByTransactionId(String transactionId, String outRefundNo, String reason,
                                                   BigDecimal refund, BigDecimal total, String notifyUrl, JSONObject commonParams) throws Exception {

        String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("transaction_id", transactionId);
        jsonObject.put("out_refund_no", outRefundNo);
        jsonObject.put("reason", reason);

        JSONObject amount = new JSONObject();
        amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
        amount.put("currency", "CNY");

        jsonObject.put("amount", amount);
        jsonObject.put("notify_url", notifyUrl);

        String result = doPost(url, jsonObject, commonParams);

        return JSONObject.parseObject(result);
    }

    /**
     * 发送HttpGet请求（调用微信支付接口专用）
     *
     * @param uriBuilder   已构建的请求路径和请求参数
     * @param commonParams 公共参数
     * @return
     * @throws Exception
     */
    public static String doGet(URIBuilder uriBuilder, JSONObject commonParams) throws Exception {
        logger.info("=====请求微信支付接口开始=====，请求地址：url=" + uriBuilder.getPath() + "，公共参数：common=" + commonParams.toJSONString());
        CloseableHttpClient httpClient = getHttpClient(commonParams.getString("privateKey"), commonParams.getString("mchId"), commonParams.getString("mchSerialNo"), commonParams.getString("apiV3Key"));

        //完成签名并执行请求
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.addHeader("Accept", "application/json");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        String result = null;
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                result = EntityUtils.toString(response.getEntity());
            } else if (statusCode == 204) {
                result = "success";
            } else {
                logger.info("=====请求微信支付接口失败=====，错误码：code=" + statusCode + "，错误信息：body=" + EntityUtils.toString(response.getEntity()));
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
        logger.info("=====请求微信支付接口结束=====，请求地址：url=" + uriBuilder.getPath() + "，返回结果：result=" + result);
        return result;
    }

    /**
     * 发送HttpPost请求（调用微信支付接口专用）
     *
     * @param url          请求地址
     * @param tradeParams  请求参数
     * @param commonParams 公共参数
     * @return
     * @throws Exception
     */
    public static String doPost(String url, JSONObject tradeParams, JSONObject commonParams) throws Exception {
        logger.info("=====请求微信支付接口开始=====，请求地址：url=" + url + "，公共参数：common=" + commonParams.toJSONString() + "，请求参数：reqData=" + tradeParams);

        CloseableHttpClient httpClient = getHttpClient(commonParams.getString("privateKey"), commonParams.getString("mchId"), commonParams.getString("mchSerialNo"), commonParams.getString("apiV3Key"));

        //请求URL
        HttpPost httpPost = new HttpPost(url);

        //请求body参数
        StringEntity entity = new StringEntity(tradeParams.toJSONString(), "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = httpClient.execute(httpPost);
        String result = null;
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                result = EntityUtils.toString(response.getEntity());
            } else if (statusCode == 204) {
                result = "success";
            } else {
                logger.info("=====请求微信支付接口失败=====，错误码：code=" + statusCode + "，错误信息：body=" + EntityUtils.toString(response.getEntity()));
                throw new IOException("request failed");
            }
        } finally {
            response.close();
            httpClient.close();
        }
        logger.info("=====请求微信支付接口结束=====，请求地址：url=" + url + "，返回结果：result=" + result);
        return result;
    }

    /**
     * 获取调用微信接口的客户端工具对象
     *
     * @param privateKey  私钥字符串
     * @param mchId       商户号
     * @param mchSerialNo 商户证书序列号
     * @param apiV3Key    V3密钥
     * @return
     * @throws Exception
     */
    public static CloseableHttpClient getHttpClient(String privateKey, String mchId, String mchSerialNo, String apiV3Key) throws Exception {
        // 加载商户私钥（privateKey：私钥字符串）
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));

        // 加载平台证书（mchId：商户号,mchSerialNo：商户证书序列号,apiV3Key：V3密钥）
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes("utf-8"));

        // 初始化httpClient
        CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier)).build();

        return httpClient;
    }
}
